package edu.caltech.android;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.location.Location;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Toast;
import edu.caltech.android.comm.AppEngineComm;
import edu.caltech.android.comm.Comm;
import edu.caltech.android.database.MeasurementDatabase;
import edu.caltech.android.picking.GMPicker;
import edu.caltech.android.picking.Pick;
import edu.caltech.android.power.PowerSettings;
import edu.caltech.android.sensor.Localizer;
import edu.caltech.android.sensor.accel.AccelSample;
import edu.caltech.android.sensor.accel.Accelerometer;
import edu.caltech.csn.data.CsnMessages.HeartbeatResponse;
import edu.caltech.csn.data.CsnMessages.StatusType;

/**
 * This is a central point of the application, which gets sensor data, applies
 * the picking algorithm, acts on messages from the cloud, and supplies data to
 * the UI activities.
 * 
 * 
 * @author mfaulk
 * 
 */
public class ShakeAlertService extends Service {
	private static final String TAG = "ShakeAlertService";
	
	/**
	 *  broadcast intent action name
	 */
	public static final String PICK = "edu.caltech.android.ShakeAlertService.PICK";
	
	/**
	 * Pick intent extras key
	 */
	public static final String PICK_KEY = "pickKey";
	
	private final boolean LOG_ACCEL = false;
	
	// Also check the BASE_URL in c2dm/AppEngineClient
	//private final String BASE_URL = "http://seismocs.appspot.com";
	private final String BASE_URL = "http://demo.latest.seismocs.appspot.com";
	
	private final IBinder binder = new ShakeAlertServiceBinder();

	// TODO: specify this 'magic number' somewhere better
	private final int heartbeatMillis = 10 * 60 * 1000;

	private GMPicker gmPicker;
	private AccelerationBroadcastReceiver accelReceiver;
	private Localizer localizer;
	private Comm comm;
	private MeasurementDatabase measurementDatabase;
	private PowerSettings powerManager; // may be null
	private Timer heartbeatTimer;
	//private PowerManager.WakeLock wakeLock;

	@Override
	public void onCreate() {
		super.onCreate();

		// ----- wake lock --------
//		PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
//		wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//		wakeLock.acquire();

		gmPicker = new GMPicker();
		localizer = new Localizer(getApplicationContext());

		try {
			Resources r = getResources();
			int[] temp = r.getIntArray(R.array.maxNumLogfiles);
			int maxNumLogfiles = temp[0];
			temp = r.getIntArray(R.array.samplesPerLogfile);
			int samplesPerLogfile = temp[0];

			measurementDatabase = MeasurementDatabase.getInstance(
					samplesPerLogfile, maxNumLogfiles, getApplicationContext());
		} catch (Exception e) {
			e.printStackTrace();
		}

		// --------- Accelerometer ------------- //
		Intent accelStartIntent = new Intent(ShakeAlertService.this,
				Accelerometer.class);
		startService(accelStartIntent);

		// register the acceleration broadcast receiver
		IntentFilter filter = new IntentFilter(Accelerometer.ACCEL_SAMPLE);
		accelReceiver = new AccelerationBroadcastReceiver();
		registerReceiver(accelReceiver, filter);

		// ---------- Registration ------------ //

		// Attempt to restore state from previous session
		// TODO: merge this with Prefs
		SharedPreferences mPrefs = getSharedPreferences(
				ShakeAlertApplication.PREFERENCE, 0);
		//
		String clientName = null;
		if (mPrefs.contains(ShakeAlertApplication.PREF_CLIENT_NAME_KEY)) {
			clientName = mPrefs.getString(
					ShakeAlertApplication.PREF_CLIENT_NAME_KEY, null);
		}
		//
		String clientID = null;
		if (mPrefs.contains(ShakeAlertApplication.PREF_CLIENT_ID_KEY)) {
			clientID = mPrefs.getString(
					ShakeAlertApplication.PREF_CLIENT_ID_KEY, null);
		}

		if ((clientName != null) && (clientID != null)) {
//			Log.v(TAG, "Restoring client name and ID: " + clientName + ", "
//					+ clientID);
			comm = new AppEngineComm(BASE_URL, clientName, clientID);
		} else {
			// Create a name for this client. This uses part of the device's
			// serial number.
			// TODO: could set this name during the SetupActivity
			//
			String androidID = "00000";
			TelephonyManager tManager = (TelephonyManager) getApplicationContext()
					.getSystemService(Context.TELEPHONY_SERVICE);
			if (tManager != null) {
				String uid = tManager.getDeviceId();
				if (uid != null) {
					//Log.v(TAG, "Got uid " + uid);
					int substringLength = Math.min(uid.length(), 5);
					androidID = uid.substring(uid.length() - substringLength,
							uid.length());
				}
			}

			clientName = Build.MODEL + androidID;

			comm = new AppEngineComm(BASE_URL, clientName);

			Location location = localizer.getLocation();
			double lat = 0.0;
			double lon = 0.0;
			if (location != null) {
				lat = location.getLatitude();
				lon = location.getLongitude();
			}
			comm.register(lat, lon);
		}

		// ---------- schedule regular heart beats --------------- //

		// TODO: check here for concurrency problems
		heartbeatTimer = new Timer("heartbeatTimer");
		heartbeatTimer.scheduleAtFixedRate(new TimerTask() {
			public void run() {

				// check if there was a HeartbeatResponse from the last
				// Heartbeat message
				HeartbeatResponse heartbeatResponse = comm
						.pollHeartbeatResponse();
				if (heartbeatResponse != null) {
					if (heartbeatResponse.getStatus() == StatusType.SUCCESS) {
						// status message
						if (heartbeatResponse.hasMessage()) {
							Log.v(TAG, "heartbeat response message: "
									+ heartbeatResponse.getMessage());
						}

						// parameter update
						if (heartbeatResponse.hasParameterUpdate()) {
//							Log.d(TAG,"Begin parsing parameter update...");
//							try {
//								ParameterUpdate update = ParameterUpdate
//										.parseFrom(heartbeatResponse
//												.getParameterUpdate());
//								GMPickerParams gmPickerParams = parseParameterUpdate(update);
//
//							} catch (Exception e) {
//								Log.d(TAG, Log.getStackTraceString(e));
//							}
						}
						
						// log request
					}
				}

				Location location = localizer.getLocation();
				String parameterVersion = Double.toString(gmPicker
						.getParameterVersion());
				if (location != null) {
					comm.heartbeat(location.getLatitude(), location
							.getLongitude(), parameterVersion);
				} else {
					Log.d(TAG, "heartbeat aborted: no location fix.");
				}
			}
		}, heartbeatMillis, heartbeatMillis);

		// --------- Power Manager ---------- //

		//Log.v(TAG, "binding to PowerManager");
		Intent bindIntent = new Intent(ShakeAlertService.this,
				PowerSettings.class);
		bindService(bindIntent, powerConnection, Context.BIND_AUTO_CREATE);

		// --------- C2DM Alerts ------------- //

		// register for intents broadcast when a new C2DM message arrives
		registerReceiver(mMessageReceiver, new IntentFilter(
				C2DMReceiver.GOT_C2DM));

	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();

		// ----- Accelerometer ------- //

		Log.d(TAG, "stopping accelerometer service...");
		Intent accelStopIntent = new Intent(ShakeAlertService.this,
				Accelerometer.class);
		stopService(accelStopIntent);
		unregisterReceiver(accelReceiver);

		// ----- Heartbeats ---------- //

		heartbeatTimer.cancel();

		// ----- Localizer ----------- //

		localizer.stop();

		// ----- Database ------------ //

		measurementDatabase.close();

		// --- Persist Registration --- //

		String clientName = comm.getClientName();
		String clientID = comm.getClientKey();
		if ((clientName != null) && (clientID != null)) {
			//Log.v(TAG, "Saving client name: " + clientName + ", key: " + clientID);
			SharedPreferences mPrefs = getSharedPreferences(
					ShakeAlertApplication.PREFERENCE, 0);
			//
			Editor editor = mPrefs.edit();
			editor
					.putString(ShakeAlertApplication.PREF_CLIENT_ID_KEY,
							clientID);
			editor.putString(ShakeAlertApplication.PREF_CLIENT_NAME_KEY,
					clientName);
			editor.commit();
		}

		// ----- PowerManager ----- //

		unbindService(powerConnection);

		// --------- C2DM Alerts ------------- //

		// unregister for intents broadcast when a new C2DM message arrives
		unregisterReceiver(mMessageReceiver);

		// ----- Wake Lock -------//
		//wakeLock.release();
	}

	/**
	 * Handles binding to the PowerManager
	 */
	private ServiceConnection powerConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName className, IBinder service) {
			// Called when the connection is made.
			powerManager = ((PowerSettings.PowerSettingsBinder) service)
					.getService();
		}

		public void onServiceDisconnected(ComponentName className) {
			// Received when the service unexpectedly disconnects.
			powerManager = null;
		}
	};

	/**
	 * receive accelerometer broadcasts
	 */
	private class AccelerationBroadcastReceiver extends BroadcastReceiver {

		@Override
		public void onReceive(Context context, Intent intent) {

			if (intent.getAction().equals(Accelerometer.ACCEL_SAMPLE)) {

				AccelSample sample = intent
						.getParcelableExtra(Accelerometer.ACCEL_SAMPLE_KEY);
				if (sample != null) {

					if(LOG_ACCEL){
						try {
							measurementDatabase.push(sample);
						} catch (IOException e) {
							Log.d(TAG, Log.getStackTraceString(e));
						}
					}
					
					// push this sample to the picking algorithm
					AccelSample m = new AccelSample(sample.x, sample.y,
							sample.z, sample.t);

					Pick pick = gmPicker.addData(m);
					if (pick != null) {
						reportPick(pick);
					}
				}
			}
		}
	} // end of class

	/**
	 * 
	 * @param p
	 */
	private void reportPick(Pick p) {
//		Toast toast = Toast.makeText(getApplicationContext(), "Pick",
//				Toast.LENGTH_SHORT);
//		toast.show();
		
		// broadcast the pick
		Intent intent = new Intent(PICK);
		intent.putExtra(PICK_KEY, p);
		sendBroadcast(intent);

		Location location = localizer.getLocation();
		if (location != null) {
			comm.pick(p, location.getLatitude(), location.getLongitude());
		} else {
			Log.d(TAG, "Pick report aborted.");
		}
	}

	/**
	 * Listen for new C2DM messages
	 */
	private final BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			CharSequence text = "CSN Alert Received!";
			int duration = Toast.LENGTH_SHORT;

			Toast toast = Toast.makeText(context, text, duration);
			toast.show();
		}
	};

	/**
	 * TODO: complete this for the new parameter update PB definition
	 * @param update
	 * @return
	 */
//	private GMPickerParams parseParameterUpdate(ParameterUpdate update) {
//		GMPickerParams p = new GMPickerParams();
//
//		if (update.hasDimensionsRetained()) {
//			p.pcaDim = update.getDimensionsRetained();
//		}
//
//		// --- construct a GaussianMixture ---
//
//		// covariance matrices
//		int sigmaCount = update.getGmSigmaCount();
//		Vector<Matrix> covariances = new Vector<Matrix>(sigmaCount);
//		List<String> sigmaStrings = update.getGmSigmaList();
//		for (String sigmaString : sigmaStrings) {
//			Matrix s = StringToMatrix.stringToMatrix(sigmaString);
//			covariances.add(s);
//		}
//
//		// means. Each column of this matrix is a mean vector.
//		// TODO: Need to break this into separate Matrix objects for each mean, and
//		// store them in a java.util.Vector
//		Matrix meansMatrix = StringToMatrix.stringToMatrix(update.getGmMu());
//		int nMeanCols = meansMatrix.getColumnDimension();
//		int nMeanRows = meansMatrix.getRowDimension();
//		Vector<Matrix> means = new Vector<Matrix>(nMeanCols);
////		for(int i=0; i< nMeanCols; i++){
////			// get a column
////			Matrix mean = meansMatrix.get
////		}
//
//		// weights
//		Matrix weights = StringToMatrix.stringToMatrix(update.getGmWeight());
//
//		//GaussianMixture gm = new GaussianMixture(weights, means, covariances);
//		// check
//
//		return p;
//	}

	@Override
	public IBinder onBind(Intent intent) {
		return binder;
	}

	//
	public class ShakeAlertServiceBinder extends Binder {
		public ShakeAlertService getService() {
			return ShakeAlertService.this;
		}
	}

	public String getClientID() {
		return comm.getClientKey();
	}

	public String getClientName() {
		return comm.getClientName();
	}

	public String getParameterVersion() {
		double paramVersion = gmPicker.getParameterVersion();
		return Double.toString(paramVersion);
	}

	public boolean gpsEnabled() {
		return localizer.gpsEnabled();
	}

	public boolean networkPositionEnabled() {
		return localizer.networkPositionEnabled();
	}

	// TODO return something standard, e.g. the BatteryManager constants.
	public String getBatteryStatus() {
		String state = "unknown";
		if (powerManager != null) {
			state = powerManager.batteryStatus;
		}
		return state;
	}

} // end of class
